import socket

from itertools import product
from pyparsing import ZeroOrMore, StringEnd, delimitedList, Group
from pyparsing import Optional, lineno, Word, nums, oneOf
from base import Base, GypsumError

import header as hdr
import helper as hp
import prefixlist as pfxlist

class JunOSPolicy(Base):
    def __init__(self, Config = None):
        super(JunOSPolicy, self).__init__()
        self._from = False
        self._terms = []
        self._protocol = set()
        self._hdr = hdr.PolicyHeader()
        self._pfx = pfxlist.PrefixList()
        hp.filedir = Config

    @hp.addlinenumber
    def _addTermHeader(self, string, location, tokens):
        term = {}
        term["name"] = tokens[0]
        if self._protocol:
            term["protocol"] = sorted(list(self._protocol))

        for ix in tokens[1:]:
            key = ix[0]
            if key == "comment":
                term["comment"] = ix[1]
            elif key in ("port", "destination-port"):
                term["dstport"] = ix[1:]
            elif key == "source-port":
                term["srcport"] = ix[1:]
            elif key == "action":
                term["action"] = self._getAction(ix[1])
            elif key == "icmp-type":
                term["icmptypes"] = ix[1:]
            elif key == "counter":
                term["counter"] = ix[1]
            elif key == "policier":
                term["policer"] = ix[1]
            elif key == "option":
                term["option"] = ix[1]
            elif key == "ttl-except":
                term["ttlexcept"] = ix[1]
            elif key == "ttl":
                term[key] = ix[1]
            elif key in ("source", "destination", "source-exclude",
                    "destination-exclude"):
                self._from = True
                retval = hp.sortaddress(ix[1:])
                if "prefix" in retval:
                    prefix = retval["prefix"]
                    if key == "source":
                        term["srcprefix"] = prefix
                    elif key == "destination":
                        term["dstprefix"] = prefix
                    elif key == "source-exclude":
                        term["srcpflex"] = prefix
                    elif key == "destination-exclude":
                        term["dstpflex"] = prefix

                if "address" in retval:
                    address = retval["address"]
                    if key == "source":
                        term["srcaddr"] = address
                    elif key == "destination":
                        term["dstaddr"] = address
                    elif key == "source-exclude":
                        term["srcadex"] = address
                    elif key == "destination-exclude":
                        term["dstadex"] = address

        if ("dstport" in term or "srcport" in term) and "protocol" in term:
            if "icmp" in term["protocol"] or 1 in term["protocol"]:
                self.error("port defined for protocol(s) %s in term %s",
                        " ".join(term["protocol"]), tokens[0])

        if "icmptypes" in term and "icmp" not in term["protocol"]:
            self.error("ICMP types defined but protocol is not ICMP in term %s",
                    tokens[0])

        term["from"] = self._from
        self._terms.append(term)

        self._protocol = set()
        self._from = False

    def _validatePortRange(self, string, location, tokens):
        start = end = 0
        retval = {}
        portrange = tokens[0]

        try:
            start = hp.validateport(portrange[0])
            end = hp.validateport(portrange[1])
        except ValueError as err:
            self.error("%s on line %d", str(err), lineno(location, string))

        if start > end:
            self.error("invalid port range on line %d", lineno(location,
                string))
        else:
            retval["start"] = start
            retval["end"] = end

        return retval

    def _validatePort(self, string, location, tokens):
        self._from = True
        retval = []
        ports = set()
        for ix in tokens:
            if isinstance(ix, dict):
                retval.append("%d-%d" %(ix["start"], ix["end"]))
            elif not isinstance(ix, dict):
                try:
                    if ix == "netconf":
                        ports.add(830)
                    elif ix == "traceroute":
                        retval.append("%d-%d" %(33434, 33534))
                    else:
                        ports.add(hp.validateport(ix))
                except ValueError as err:
                    self.error("invalid port %s on line %d", ix, lineno(
                        location, string))

        if len(ports):
            retval += sorted(list(ports))

        return retval

    def _validateIcmp4(self, string, location, tokens):
        self._from = True
        icmpval = sorted(set(hp.icmp4.values()))
        icmpkey = sorted(set(hp.icmp4.keys()))

        retval = set()
        for ix in tokens:
            if ix.isdigit() and int(ix) in icmpval:
                retval.add(int(ix))
            elif ix.isalpha() and ix in icmpkey:
                retval.add(hp.icmp4[ix])
            else:
                if ix in icmpkey:
                    retval.add(hp.icmp4[ix])
                else:
                    self.error("invalid icmp type %s on line %d", ix, lineno(
                        location, string))

        return sorted(list(retval))

    def _validateProtocol(self, string, location, tokens):
        self._from = True
        for ix in tokens:
            if ix.isalpha():
                try:
                    temp = socket.getprotobyname(ix)
                except socket.error as err:
                    self.error("invalid protocol %s on line %d", ix, lineno(
                        location, string))
            elif ix.isdigit():
                jy = int(ix)
                if jy < 0 or jy > 255:
                    self.error("invalid protocol number %d on line %d", jy,
                            lineno(location, string))

            self._protocol.add(ix)

    def _established(self, string, location, tokens):
        self._from = True
        retval = []
        proto = list(self._protocol)
        if len(proto) == 1:
            if tokens[0] == "tcp-established" and proto[0] == "tcp":
                retval = ["option", tokens[0]]
            elif tokens[0] == "established" and proto[0] == "udp":
                retval = ["port", "1024-65535"]
            else:
                retval = ["option", tokens[0]]

        return [retval]

    def _addTermFile(self, string, location, tokens):
        filename = tokens[0]
        term = JunOSPolicy(hp.filedir)
        try:
            grammar = (term.pflgrammar & term.termgrammar) + StringEnd()
            results = hp.parser(filename, grammar)
        except GypsumError as err:
            self.error(err)
        else:
            for ix in term.value:
                self._terms.append(ix)

    def _addTermFromFile(self, string, location, tokens):
        filename = tokens[-1]
        termlist = tokens[0:len(tokens) - 1]
        term = JunOSPolicy(hp.filedir)
        try:
            grammar = term.termgrammar + StringEnd()
            results = hp.parser(filename, grammar)
        except GypsumError as err:
            self.error(str(err))
        else:
            for (ix, jy) in product(termlist, term.value):
                if ix == jy["name"]:
                    self._terms.append(jy)

    def _validateTTL(self, string, location, tokens):
        self._from = True
        retval = []
        (ttlopt, value) = tokens[0]
        value = int(value)
        if value < 0 or value > 255:
            self.error("ttl value must be between 1 - 255, value: %d, line: %d",
                    value, lineno(location, string))
        else:
            retval = [ttlopt, value]

        return [retval]

    def _getAction(self, action):
        retval = None
        if action in ("accept", "permit", "allow"):
            retval = "accept"
        elif action in ("deny", "disallow"):
            retval = "discard"
        else:
            retval = action

        return retval

    @property
    def termgrammar(self):
        """
        parsing ports:
            destination-port = list of ports
            source-port = list of ports
            port = list of ports
        """
        portrange = Group(hp.identifier + hp.colon +
                hp.identifier).setParseAction(self._validatePortRange)

        portopts = delimitedList(portrange | hp.identifier).setParseAction(
                self._validatePort)
        port = hp.addStmt("port", portopts)
        dstport = hp.addStmt("destination-port", portopts)
        srcport = hp.addStmt("source-port", portopts)

        icmpopts = delimitedList(hp.identifier | hp.integer).setParseAction(
                self._validateIcmp4)
        icmptype = hp.addStmt("icmp-type", icmpopts)

        protocol = hp.addStmt("protocol", delimitedList(
            hp.identifier).setParseAction(self._validateProtocol))

        prefixopts = delimitedList(hp.ipaddr | hp.identifier)
        src = hp.addStmt("source", prefixopts)
        dst = hp.addStmt("destination", prefixopts)
        srcex = hp.addStmt("source-exclude", prefixopts)
        dstex = hp.addStmt("destination-exclude", prefixopts)

        action = hp.addStmt("action", hp.action)
        counter = hp.addStmt("counter", hp.identifier)
        policer = hp.addStmt("policer", hp.identifier)
        ttl = (hp.addStmt("ttl", hp.integer) | hp.addStmt("ttl-except",
            hp.integer)).setParseAction(self._validateTTL)
        option = oneOf("established tcp-established sample initial rst \
                is-fragment first-fragment").setParseAction(self._established)

        termopts = (
                hp.comment &
                action &
                Optional(port | dstport) &
                Optional(srcport) &
                Optional(icmptype) &
                Optional(protocol) &
                Optional(counter) &
                Optional(policer) &
                Optional(src) &
                Optional(dst) &
                Optional(srcex) &
                Optional(dstex) &
                Optional(ttl) &
                Optional(option)
                )

        stdterm = (hp.kword("term") + hp.identifier + hp.lb + termopts +
                    hp.rb).setParseAction(self._addTermHeader)
        termfile = (hp.kword("term") + hp.lt + hp.filename).setParseAction(
                self._addTermFile)
        incterm = (hp.kword("term") + delimitedList(hp.identifier) + hp.lt +
                hp.filename).setParseAction(self._addTermFromFile)

        return (ZeroOrMore(stdterm) &
                ZeroOrMore(incterm) &
                ZeroOrMore(termfile))

    @property
    def grammar(self):
        return self._hdr.grammar + (
                self.pflgrammar & self.termgrammar) + StringEnd()

    @property
    def header(self):
        return self._hdr.value

    @property
    def prefixlist(self):
        return self._pfx.value

    @property
    def value(self):
        return self._terms

    @property
    def pflgrammar(self):
        return ZeroOrMore(self._pfx.grammar)
